package com.tymoshenko.seabattle.server.shooting;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.tymoshenko.seabattle.client.rpc.shooting.ShootingService;
import com.tymoshenko.seabattle.shared.model.domain.*;
import org.apache.log4j.Logger;

import java.util.*;

/**
 * @author Yakiv Tymoshenko
 * @since 08.02.13
 */
public class ShootingServiceImpl extends RemoteServiceServlet implements ShootingService {

    private static final Logger LOGGER = Logger.getLogger(ShootingServiceImpl.class);

    public static Random random = new Random();

    public List<Point> returnComputerTargets(Player humanPlayer) {
        List<Point> targets = new ArrayList<Point>();

        Board.Status statusBeforeShot;
        int shotNumber = 1;
        Fleet opponentFleet = humanPlayer.getFleet();
        Map<Point, Board.Status> opponentFields = new HashMap<Point, Board.Status>(humanPlayer.getBoard().getFields());
        boolean wasDamaged = false;
        Point previousTarget = null;
        do {
            Point nextTarget;
            Ship unfinishedShip = findUnfinishedShip(opponentFields, opponentFleet);
            if (wasDamaged && unfinishedShip == null) {
                nextTarget = tryToSinkDamagedShip(opponentFields, opponentFleet, previousTarget);
                if (nextTarget == null) {
                    nextTarget = findRandomTarget(opponentFields);
                }
            } else if (unfinishedShip != null) {
                nextTarget = tryToSinkDamagedShip(opponentFields, unfinishedShip);
            } else {
                nextTarget = findRandomTarget(opponentFields);
            }
            statusBeforeShot = opponentFields.get(nextTarget);
            wasDamaged = hasDamaged(opponentFleet, nextTarget);
            shoot(opponentFields, opponentFleet, nextTarget);
            targets.add(nextTarget);
            previousTarget = nextTarget;
            LOGGER.info("Shot # " + shotNumber++ + " Target: " + nextTarget + " statusBeforeShot: " + statusBeforeShot);
        } while (wasDamaged);

        LOGGER.info("Targets: ");
        for (Point target : targets) {
            LOGGER.info(target);
        }

        return targets;
    }

    private Ship findUnfinishedShip(Map<Point, Board.Status> opponentFields, Fleet opponentFleet) {
        for (Ship ship : opponentFleet.getShips()) {
            for (Point deck : ship.getDecks()) {
                if (Board.Status.DAMAGED.equals(opponentFields.get(deck))) {
                    return ship;
                }
            }
        }
        return null;
    }

    private Point tryToSinkDamagedShip(Map<Point, Board.Status> opponentFields, Fleet opponentFleet, Point previousTarget) {
        Point nextTarget;
        List<Point> targetableFields = targetableFields(opponentFields);
        Ship ship = opponentFleet.getShipByDeck(previousTarget);
//        Ship ship = findUnfinishedShip(opponentFields, opponentFleet);
        if (ship == null) {
            return null;
        }
        if (hasSheepMoreThanTwoDecksDamaged(opponentFields, ship)) {
            Ship.Direction direction = ship.getDirection();
            nextTarget = findNextTargetByDirection(targetableFields, previousTarget, direction);
        } else {
            nextTarget = findNeighbour(targetableFields, previousTarget);
        }
        return nextTarget;
    }

    private Point tryToSinkDamagedShip(Map<Point, Board.Status> opponentFields, Ship ship) {
        Point nextTarget = null;
        List<Point> targetableFields = targetableFields(opponentFields);
        if (hasSheepMoreThanTwoDecksDamaged(opponentFields, ship)) {
            Ship.Direction direction = ship.getDirection();
            List<Point> damagedDecks = findDamagedDecks(opponentFields, ship);
            for (Point previousTarget : damagedDecks) {
                nextTarget = findNextTargetByDirection(targetableFields, previousTarget, direction);
                if (nextTarget != null) {
                    break;
                }
            }
        } else {
            Point previousTarget;
            for (Point damagedDeck : ship.getDecks()) {
                if (Board.Status.DAMAGED.equals(opponentFields.get(damagedDeck))) {
                    previousTarget = damagedDeck;
                    nextTarget = findNeighbour(targetableFields, previousTarget);
                    if (nextTarget != null) {
                        break;
                    }
                }
            }

        }
        return nextTarget;
    }

    private List<Point> findDamagedDecks(Map<Point, Board.Status> opponentFields, Ship ship) {
        List<Point> damaged = new ArrayList<Point>();
        for (Point point : ship.getDecks()) {
            if (Board.Status.DAMAGED.equals(opponentFields.get(point))) {
                damaged.add(point);
            }
        }
        return damaged;
    }

    private Point findNeighbour(List<Point> targetableFields, Point previousTarget) {
        Point upper = Point.valueOf(previousTarget.getRow() - 1, previousTarget.getCol());
        Point lower = Point.valueOf(previousTarget.getRow() + 1, previousTarget.getCol());
        Point left = Point.valueOf(previousTarget.getRow(), previousTarget.getCol() - 1);
        Point right = Point.valueOf(previousTarget.getRow(), previousTarget.getCol() + 1);
        if (targetableFields.contains(upper)) {
            return upper;
        }
        if (targetableFields.contains(lower)) {
            return lower;
        }
        if (targetableFields.contains(left)) {
            return left;
        }
        if (targetableFields.contains(right)) {
            return right;
        }
        return null;
    }

    private Point findNextTargetByDirection(List<Point> targetableFields, Point previousTarget, Ship.Direction direction) {
        Point nextTarget;
        if (Ship.Direction.HORIZONTAL.equals(direction)) {
            nextTarget = Point.valueOf(previousTarget.getRow(), previousTarget.getCol() - 1);
            if (targetableFields.contains(nextTarget)) {
                return nextTarget;
            }
            nextTarget = Point.valueOf(previousTarget.getRow(), previousTarget.getCol() + 1);
            if (targetableFields.contains(nextTarget)) {
                return nextTarget;
            }
        } else {
            nextTarget = Point.valueOf(previousTarget.getRow() - 1, previousTarget.getCol());
            if (targetableFields.contains(nextTarget)) {
                return nextTarget;
            }
            nextTarget = Point.valueOf(previousTarget.getRow() + 1, previousTarget.getCol());
            if (targetableFields.contains(nextTarget)) {
                return nextTarget;
            }
        }
        return null;
    }

    private boolean hasSheepMoreThanTwoDecksDamaged(Map<Point, Board.Status> opponentFields, Ship ship) {
        int damagedDecks = 0;
        for (Point deck : ship.getDecks()) {
            if (Board.Status.DAMAGED.equals(opponentFields.get(deck))) {
                damagedDecks++;
            }
        }
        return damagedDecks > 1;
    }

    private boolean hasDamaged(Fleet fleet, Point target) {
        return target != null && fleet.getShipByDeck(target) != null;
    }

    private void shoot(Map<Point, Board.Status> fields, Fleet fleet, Point target) {
        Board.Status statusBeforeShot = fields.get(target);
        if (Board.Status.DECK.equals(statusBeforeShot)) {
            fields.put(target, Board.Status.DAMAGED);
            if (isShipDestroyed(fields, fleet, target)) {
                markDestroyed(fields, fleet, target);
            }
        } else if (Board.Status.FREE.equals(statusBeforeShot) || Board.Status.BORDER.equals(statusBeforeShot)) {
            fields.put(target, Board.Status.MISSED);
        }
    }

    private void markDestroyed(Map<Point, Board.Status> fields, Fleet fleet, Point target) {
        Ship shipUnderAttack = fleet.getShipByDeck(target);
        if (shipUnderAttack != null) {
            for (Point deck : shipUnderAttack.getDecks()) {
                fields.put(deck, Board.Status.DESTROYED);
            }
            List<Point> shipBorderWaters = Board.shipNeighbourFields(shipUnderAttack);
            for (Point border : shipBorderWaters) {
                fields.put(border, Board.Status.MISSED);
            }
            fleet.destroyShip(shipUnderAttack);
        }
    }

    private boolean isShipDestroyed(Map<Point, Board.Status> fields, Fleet fleet, Point target) {
        boolean result = true;
        Ship shipUnderAttack = fleet.getShipByDeck(target);
        if (shipUnderAttack != null) {
            for (Point deck : shipUnderAttack.getDecks()) {
                if (Board.Status.DECK.equals(fields.get(deck))) {
                    result = false;
                    break;
                }
            }
        } else {
            result = false;
        }
        return result;
    }

    private Point findRandomTarget(Map<Point, Board.Status> fields) {
        List<Point> targetableFields = targetableFields(fields);
        return targetableFields.get(random.nextInt(targetableFields.size()));
    }

    private List<Point> targetableFields(Map<Point, Board.Status> fields) {
        List<Point> result = new ArrayList<Point>(Board.HEIGHT * Board.WIDTH);
        for (Map.Entry<Point, Board.Status> field : fields.entrySet()) {
            if (field.getValue() != Board.Status.MISSED && field.getValue() != Board.Status.DAMAGED && field.getValue() != Board.Status.DESTROYED) {
                result.add(field.getKey());
            }
        }
        return result;
    }
}
